﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Services.Client;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Xceed.Silverlight.Data
{
  public abstract class WcfDataServicesDataSourceProviderBase<TElement> : WcfDataServicesDataSourceProviderBase
  {
    #region Static Fields

    private static readonly Guid ExecuteCountQueryCallId = DataUtils.CreateCallId();
    private static readonly Guid ExecuteDataQueryCallId = DataUtils.CreateCallId();

    #endregion

    #region Constructors

    protected WcfDataServicesDataSourceProviderBase(
      DataServiceQuery<TElement> dataServiceQuery,
      DataServiceContext dataServiceContext )
      : base( dataServiceContext )
    {
      if( dataServiceQuery == null )
        throw new ArgumentNullException( "dataServiceQuery" );

      this.Initialize( dataServiceQuery );
    }

    internal WcfDataServicesDataSourceProviderBase(
      DataServiceQuery<TElement> dataServiceQuery )
      : base()
    {
      if( dataServiceQuery == null )
        throw new ArgumentNullException( "dataServiceQuery" );

      this.Initialize( dataServiceQuery );
    }

    #endregion

    #region CanCount Property

    public override bool CanCount
    {
      get
      {
        return m_canCount;
      }
    }

    private bool m_canCount; //false

    #endregion

    #region DataServiceQuery Protected Property

    protected DataServiceQuery<TElement> DataServiceQuery
    {
      get;
      private set;
    }

    #endregion

    #region ElementType Protected Property

    protected internal sealed override Type ElementType
    {
      get
      {
        return this.DataServiceQuery.ElementType;
      }
    }

    #endregion

    protected virtual DataServiceQuery<TElement> GetDataServiceQuery( CountQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.FilterGroup == null )
                 && ( queryParameters.Jump == null ) );

      DataServiceQuery<TElement> dataServiceQuery = this.DataServiceQuery;

      if( queryParameters.Filter != null )
      {
        SortRequest sortBy = ( queryParameters.Sort != null )
                               ? queryParameters.Sort
                               : new SortRequest( this.PrimaryKeyInfo );

        dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.OrderBy( dataServiceQuery, sortBy );
        dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Where( dataServiceQuery, queryParameters.Filter );
      }

      dataServiceQuery = ( DataServiceQuery<TElement> )dataServiceQuery.Take( 0 );

      return dataServiceQuery;
    }

    protected virtual DataServiceQuery<TElement> GetDataServiceQuery( DataQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.ItemJump == null )
                 && ( queryParameters.Jump == null ) );

      DataServiceQuery<TElement> dataServiceQuery = this.DataServiceQuery;

      dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.OrderBy( dataServiceQuery, queryParameters.Sort );

      if( queryParameters.Filter != null )
      {
        if( queryParameters.Sort == null )
        {
          dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.OrderBy( dataServiceQuery, new SortRequest( this.PrimaryKeyInfo ) );
        }

        dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Where( dataServiceQuery, queryParameters.Filter );
      }

      dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Skip( dataServiceQuery, queryParameters.Skip );
      dataServiceQuery = WcfDataServicesDataSourceProviderBase<TElement>.Take( dataServiceQuery, queryParameters.Take );

      return dataServiceQuery;
    }

    protected override IAsyncResult BeginExecuteCountQuery(
      CountQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      AsyncResult asyncResult = new AsyncResult(
                                  WcfDataServicesDataSourceProviderBase<TElement>.ExecuteCountQueryCallId,
                                  callback,
                                  state );

      DataServiceQuery<TElement> query = this.GetDataServiceQuery( queryParameters );

      asyncResult.Start(
        ( cb, st ) => query.BeginExecute( cb, query ),
        null );

      return asyncResult;
    }

    protected override CountQueryResult EndExecuteCountQuery( IAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( WcfDataServicesDataSourceProviderBase<TElement>.ExecuteCountQueryCallId );

      IAsyncResult ar = asyncResult.GetLastAsyncResult<IAsyncResult>();
      DataServiceQuery<TElement> query = ( DataServiceQuery<TElement> )ar.AsyncState;
      QueryOperationResponse response = ( QueryOperationResponse )query.EndExecute( ar );

      if( response.Error != null )
        throw response.Error;

      long count = response.TotalCount;

      return new CountQueryResult( count );
    }

    protected override IAsyncResult BeginExecuteDataQuery(
      DataQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      AsyncResult asyncResult = new AsyncResult(
                                  WcfDataServicesDataSourceProviderBase<TElement>.ExecuteDataQueryCallId,
                                  callback,
                                  state );

      DataServiceQuery<TElement> query = this.GetDataServiceQuery( queryParameters );
      PartialDataResult queryResult = new PartialDataResult( query );

      asyncResult.Start(
        ( cb, st ) => query.BeginExecute( cb, queryResult ),
        ( par, iar ) => this.HandleContinuation( iar ) );

      return asyncResult;
    }

    protected override DataQueryResult EndExecuteDataQuery( IAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( WcfDataServicesDataSourceProviderBase<TElement>.ExecuteDataQueryCallId );

      PartialDataResult queryResult = asyncResult.GetCallResult<PartialDataResult>();

      Debug.Assert( queryResult != null );

      if( queryResult.Error != null )
        throw queryResult.Error;

      return new DataQueryResult( queryResult.Data, queryResult.InlineCount );
    }

    private DependentCallResult HandleContinuation( IAsyncResult asyncResult )
    {
      PartialDataResult queryResult = ( PartialDataResult )asyncResult.AsyncState;
      DataServiceQuery<TElement> query = queryResult.DataServiceQuery;
      QueryOperationResponse<TElement> response = ( QueryOperationResponse<TElement> )query.EndExecute( asyncResult );

      // An error occured, there is nothing left to fetch. 
      if( response.Error != null )
      {
        queryResult.Error = response.Error;

        return new DependentCallResult( queryResult );
      }

      // Retrieve the inline count.
      queryResult.InlineCount = WcfDataServicesDataSourceProviderBase<TElement>.GetInlineCount( response );

      // Retrieve the items downloaded.
      queryResult.Data.AddRange( response );

      // Find out if there is a continuation.
      DataServiceQueryContinuation<TElement> continuation = response.GetContinuation();

      // There is nothing left.  We have all the data that was requested.
      if( continuation == null )
        return new DependentCallResult( queryResult );

      // We cannot handle a continuation if we don't have the DataServiceContext.
      if( this.DataServiceContext == null )
      {
        try
        {
          // The exception must be thrown on the call to EndExecuteDataQuery.
          // We throw the exception within a try/catch block to get a call stack.
          throw new DataInternalException();
        }
        catch( Exception e )
        {
          queryResult.Error = e;
        }

        return new DependentCallResult( queryResult );
      }

      return new DependentCallResult(
        queryResult,
        ( cb, st ) => this.BeginExecuteDataContinuation( continuation, cb, st ),
        ( par, iar ) => this.EndExecuteDataContinuation( iar ) );
    }

    private IAsyncResult BeginExecuteDataContinuation(
      DataServiceQueryContinuation<TElement> continuation,
      AsyncCallback callback,
      object state )
    {
      return this.DataServiceContext.BeginExecute( continuation, callback, state );
    }

    private DependentCallResult EndExecuteDataContinuation( IAsyncResult asyncResult )
    {
      PartialDataResult queryResult = ( PartialDataResult )asyncResult.AsyncState;
      QueryOperationResponse<TElement> response = ( QueryOperationResponse<TElement> )this.DataServiceContext.EndExecute<TElement>( asyncResult );

      // An error occured, there is nothing left to fetch. 
      if( response.Error != null )
      {
        queryResult.Error = response.Error;

        return new DependentCallResult( queryResult );
      }

      // Retrieve the items downloaded.
      queryResult.Data.AddRange( response );

      // Find out if there is a continuation.
      DataServiceQueryContinuation<TElement> continuation = response.GetContinuation();

      // There is nothing left.  We have all the data that was requested.
      if( continuation == null )
        return new DependentCallResult( queryResult );

      return new DependentCallResult(
        queryResult,
        ( cb, st ) => this.BeginExecuteDataContinuation( continuation, cb, st ),
        ( par, iar ) => this.EndExecuteDataContinuation( iar ) );
    }

    private void Initialize( DataServiceQuery<TElement> dataServiceQuery )
    {
      this.DataServiceQuery = dataServiceQuery;

      m_canCount = WcfDataServicesDataSourceProvider<TElement>.IsCountingEnabled( dataServiceQuery.RequestUri );
    }

    private static Nullable<long> GetInlineCount( QueryOperationResponse response )
    {
      if( WcfDataServicesDataSourceProvider<TElement>.IsCountingEnabled( response.Query.RequestUri ) )
        return response.TotalCount;

      return null;
    }

    private static bool IsCountingEnabled( Uri uri )
    {
      return ( uri != null )
          && ( uri.Query.ToLowerInvariant().Contains( "$inlinecount=allpages" ) );
    }

    private static DataServiceQuery<TElement> Take(
      DataServiceQuery<TElement> dataServiceQuery,
      TakeRequest request )
    {
      if( request == null )
        return dataServiceQuery;

      return ( DataServiceQuery<TElement> )dataServiceQuery.Take<TElement>( request.Count );
    }

    private static DataServiceQuery<TElement> Skip(
      DataServiceQuery<TElement> dataServiceQuery,
      SkipRequest request )
    {
      if( ( request == null ) || ( request.Count == 0 ) )
        return dataServiceQuery;

      IQueryable<TElement> query = dataServiceQuery;
      long skip = request.Count;

      while( skip > 0 )
      {
        if( skip > int.MaxValue )
        {
          query = query.Skip<TElement>( int.MaxValue );
          skip -= int.MaxValue;
        }
        else
        {
          query = query.Skip<TElement>( Convert.ToInt32( skip ) );
          skip = 0;
        }
      }

      return ( DataServiceQuery<TElement> )query;
    }

    private static DataServiceQuery<TElement> Where(
      DataServiceQuery<TElement> dataServiceQuery,
      FilterRequest request )
    {
      if( request == null )
        return dataServiceQuery;

      Debug.Assert( ( request.Filter == null ) && ( request.Expression != null ) );

      ExpressionBuilder eb = new ExpressionBuilder( request.Expression );
      Expression<Func<TElement, bool>> predicate = eb.ToExpression<TElement>();

      if( predicate == null )
        return dataServiceQuery;

      return ( DataServiceQuery<TElement> )dataServiceQuery.Where<TElement>( predicate );
    }

    private static DataServiceQuery<TElement> OrderBy(
      DataServiceQuery<TElement> dataServiceQuery,
      SortRequest request )
    {
      if( ( request == null ) || ( request.SortDescriptions.Count <= 0 ) )
        return dataServiceQuery;

      bool firstPass = true;
      IQueryable<TElement> query = dataServiceQuery;

      foreach( SortDescription sortBy in request.SortDescriptions )
      {
        // Create the key selector that will target the sorted field.
        bool isNullableExpression;
        ParameterExpression parameter = Expression.Parameter( typeof( TElement ), null );
        Expression getMember = ComplexStringHelper.GetExpressionFromString( sortBy.PropertyName, typeof( TElement ), parameter, false, out isNullableExpression );
        Type delegateType = typeof( Func<,> ).MakeGenericType( typeof( TElement ), getMember.Type );
        Expression getter = Expression.Lambda( delegateType, getMember, new ParameterExpression[] { parameter } );

        // Find out the name of the ordering method to call.
        bool ascending = ( sortBy.Direction == ListSortDirection.Ascending );
        string methodName;

        if( firstPass )
        {
          methodName = ( ascending ) ? "OrderBy" : "OrderByDescending";
        }
        else
        {
          methodName = ( ascending ) ? "ThenBy" : "ThenByDescending";
        }

        // Retrieve the method to call to sort the values.
        MethodInfo orderingMethodInfo = null;
        foreach( MethodInfo methodInfo in typeof( Queryable ).GetMethods( BindingFlags.Public | BindingFlags.Static ) )
        {
          if( ( methodInfo.Name == methodName )
            && ( methodInfo.IsGenericMethod )
            && ( methodInfo.GetGenericArguments().Length == 2 )
            && ( methodInfo.GetParameters().Length == 2 ) )
          {
            orderingMethodInfo = methodInfo.MakeGenericMethod( typeof( TElement ), getMember.Type );
            break;
          }
        }

        // Invoke the ordering method.
        query = ( IQueryable<TElement> )orderingMethodInfo.Invoke( null, new object[] { query, getter } );
        firstPass = false;
      }

      return ( DataServiceQuery<TElement> )query;
    }

    #region PartialDataResult Nested Type

    private class PartialDataResult
    {
      internal PartialDataResult( DataServiceQuery<TElement> dataServiceQuery )
      {
        m_dataServiceQuery = dataServiceQuery;
      }

      internal Exception Error
      {
        get
        {
          return m_error;
        }
        set
        {
          if( m_error == null )
          {
            m_error = value;
          }
        }
      }

      internal List<TElement> Data
      {
        get
        {
          return m_data;
        }
      }

      internal Nullable<long> InlineCount
      {
        get;
        set;
      }

      internal DataServiceQuery<TElement> DataServiceQuery
      {
        get
        {
          return m_dataServiceQuery;
        }
      }

      private List<TElement> m_data = new List<TElement>();
      private Exception m_error; //null
      private DataServiceQuery<TElement> m_dataServiceQuery;
    }

    #endregion

    #region ExpressionBuilder Nested Type

    private class ExpressionBuilder : PredicateExpressionBuilder
    {
      internal ExpressionBuilder( FilterExpressionBase expression )
        : base( expression )
      {
      }

      protected override Expression<Func<T, bool>> CreateTautology<T>()
      {
        return null;
      }
    }

    #endregion
  }
}
